﻿// <copyright file="WP7LinqToSQLGenerator.cs"  company="$registerdorganization$">
// Copyright (c) 2011 Rashans . All rights reserved.
// </copyright>
// <author>Rashan Anushka</author>
// <email>admin@rashans.com</email>
// <date>2012-03-18</date>
// <summary>Linq to SQL code generator for Windows Phone</summary>

namespace Rashans.WP7LinqToSQLGenerator
{
    using System;
    using System.CodeDom;
    using System.CodeDom.Compiler;
    using System.IO;
    using System.Linq;
    using System.Runtime.InteropServices;
    using System.Text;
    using System.Xml;
    using System.Xml.Schema;
    using Microsoft.VisualStudio.Shell;
    using Microsoft.VisualStudio.Shell.Interop;

    /// <summary>
    /// Linq to SQL code generator for Windows Phone
    /// </summary>
    [ComVisible(true)]
    [Guid(GuidList.guidWP7LinqToSQLGenerator)]
    [CodeGeneratorRegistration(typeof(WP7LinqToSQLGenerator), "C# WP7LinqToSql Generator", "{FAE04EC1-301F-11D3-BF4B-00C04F79EFBC}", GeneratesDesignTimeSource = true)]
    [CodeGeneratorRegistration(typeof(WP7LinqToSQLGenerator), "VB WP7LinqToSql Generator", "{164B10B9-B200-11D0-8C61-00A0C91E29D5}", GeneratesDesignTimeSource = true)]
    [Microsoft.VisualStudio.Shell.ProvideObject(typeof(WP7LinqToSQLGenerator))]
    public class WP7LinqToSQLGenerator : BaseCodeGeneratorWithSite
    {
#pragma warning disable 0414
        /// <summary>
        /// The schema file name of the DBML files
        /// </summary>
        private const string SchemaFileName = "Rashans.WP7LinqToSQLGenerator.DBMLSchema.xsd";

        /// <summary>
        /// The name of this generator (use for 'Custom Tool' property of project item)
        /// </summary>
        internal static string name = "WP7LinqToSQLGenerator";
#pragma warning restore 0414

        /// <summary>
        /// The result of validating the XML content
        /// </summary>
        private static bool validXML;

        /// <summary>
        /// the method that does the actual work of generating code given the input
        /// file.
        /// </summary>
        /// <param name="inputFileContent">file contents as a string</param>
        /// <returns>
        /// the generated code file as a byte-array
        /// </returns>
        protected override byte[] GenerateCode(string inputFileContent)
        {
            // Validate the XML file against the schema
            using (StringReader contentReader = new StringReader(inputFileContent))
            {
                this.VerifyDocumentSchema(contentReader);
            }

            if (!validXML)
            {
                // Returning null signifies that generation has failed
                return null;
            }

            try
            {
                CodeDomProvider provider = GetCodeProvider();
                CodeGeneratorOptions options = GetCodeOptions();
                CodeCompileUnit compileUnit = new CodeCompileUnit();

                // Just for VB.NET:
                // Option Strict On (controls whether implicit type conversions are allowed)
                compileUnit.UserData.Add("AllowLateBound", false);

                // Option Explicit On (controls whether variable declarations are required)
                compileUnit.UserData.Add("RequireVariableDeclaration", true);

                // Code Namespace
                CodeNamespace codeNamespace = new CodeNamespace(FileNameSpace);

                // Add the imports
                codeNamespace.Imports.Add(new CodeNamespaceImport("System"));
                codeNamespace.Imports.Add(new CodeNamespaceImport("System.ComponentModel"));
                codeNamespace.Imports.Add(new CodeNamespaceImport("System.Data.Linq"));
                codeNamespace.Imports.Add(new CodeNamespaceImport("System.Data.Linq.Mapping"));

                // Create and add the base class to namespace
                codeNamespace.Types.Add(SourceCodeGenerator.CreateBaseClassDeclaration("LocalEntityBase"));

#if UseTypedClasses
                // De-serialize the document
                Database dataBase = null;
                var deserializer = new System.Xml.Serialization.XmlSerializer(typeof(Database));
                using (StringReader stream = new StringReader(inputFileContent))
                {
                    dataBase = deserializer.Deserialize(stream) as Database;
                }

                if (dataBase == null)
                {
                    return null;
                }

                // Create the declaration for the DataContext from the passed-in XML file
                codeNamespace.Types.Add(SourceCodeGenerator.CreateDataContextDeclaration(dataBase));

                // Generate the entity classes
                var entityDeclarations = SourceCodeGenerator.CreateEntityDeclarations(dataBase);

#else
                // Load the DBML file
                XmlDocument doc = new XmlDocument();
                doc.LoadXml(inputFileContent);

                // Create the declaration for the DataContext from the passed-in XML file
                codeNamespace.Types.Add(SourceCodeGenerator.CreateDataContextDeclaration(ref doc));

                // Generate the entity classes
                var entityDeclarations = SourceCodeGenerator.CreateEntityDeclarations(ref doc);
#endif
                // Add entity declarations
                codeNamespace.Types.AddRange(entityDeclarations.ToArray());

                // Add the namespace to the compile unit
                compileUnit.Namespaces.Add(codeNamespace);

                using (StringWriter sourceWriter = new StringWriter())
                {
                    provider.GenerateCodeFromCompileUnit(compileUnit, sourceWriter, options);

                    // Generate the output (source code)
                    sourceWriter.Flush();

                    return GetContentBytes(sourceWriter);
                }
            }
            catch (Exception e)
            {
                this.GeneratorError(4, e.ToString(), 1, 1);

                // Returning null signifies that generation has failed
                return null;
            }
        }

        /// <summary>
        /// Gets the code options.
        /// </summary>
        /// <returns>
        /// the code generation options
        /// </returns>
        private static CodeGeneratorOptions GetCodeOptions()
        {
            CodeGeneratorOptions options = new CodeGeneratorOptions();
            options.BlankLinesBetweenMembers = true;
            options.BracingStyle = "C";
            return options;
        }

        /// <summary>
        /// Gets the content bytes.
        /// </summary>
        /// <param name="writer">The writer.</param>
        /// <returns>the contents as a byte array</returns>
        private static byte[] GetContentBytes(StringWriter writer)
        {
            // Get the Encoding used by the writer. We're getting the WindowsCodePage encoding,
            // which may not work with all languages
            Encoding enc = Encoding.GetEncoding(writer.Encoding.WindowsCodePage);

            // Get the preamble (byte-order mark) for our encoding
            byte[] preamble = enc.GetPreamble();
            int preambleLength = preamble.Length;

            // Convert the writer contents to a byte array
            byte[] body = enc.GetBytes(writer.ToString());

            // Prepend the preamble to body (store result in resized preamble array)
            Array.Resize<byte>(ref preamble, preambleLength + body.Length);
            Array.Copy(body, 0, preamble, preambleLength, body.Length);

            // Return the combined byte array
            return preamble;
        }

        /// <summary>
        /// Receives any errors that occur while validating the documents's schema.
        /// </summary>
        /// <param name="sender">Sender object</param>
        /// <param name="args">Details about the validation error that has occurred</param>
        private void OnSchemaValidationError(object sender, ValidationEventArgs args)
        {
            // signal that validation of document against schema has failed
            validXML = false;

            // Report the error (so that it is shown in the error list)
            this.GeneratorError(4, args.Exception.Message, (uint)args.Exception.LineNumber - 1, (uint)args.Exception.LinePosition - 1);
        }

        /// <summary>
        /// Verify the XML document in contentReader against the schema in DBMLSchema.xsd
        /// </summary>
        /// <param name="contentReader">TextReader for XML document</param>
        private void VerifyDocumentSchema(TextReader contentReader)
        {
            // Options for XmlReader object can be set only in constructor. After the object is created,
            // they become read-only. Because of that we need to create XmlSettings structure,
            // fill it in with correct parameters and pass into XmlReader constructor.
            XmlReaderSettings validatorSettings = new XmlReaderSettings();
            validatorSettings.ValidationType = ValidationType.Schema;
            validatorSettings.XmlResolver = null;
            validatorSettings.ValidationEventHandler += new ValidationEventHandler(this.OnSchemaValidationError);

            // Schema is embedded in this assembly. Get its stream
            Stream schema = this.GetType().Assembly.GetManifestResourceStream(SchemaFileName);

            using (XmlTextReader schemaReader = new XmlTextReader(schema))
            {
                try
                {
                    validatorSettings.Schemas.Add("http://schemas.microsoft.com/linqtosql/dbml/2007", schemaReader);
                }
                catch (XmlException e)
                {
                    this.GeneratorError(4, "InvalidSchemaFileEmbeddedInGenerator " + e.ToString(), 1, 1);
                    validXML = false;
                    return;
                }
                catch (XmlSchemaException e)
                {
                    this.GeneratorError(4, "InvalidSchemaFileEmbeddedInGenerator " + e.ToString(), 1, 1);
                    validXML = false;
                    return;
                }

                using (XmlReader validator = XmlReader.Create(contentReader, validatorSettings, InputFilePath))
                {
                    validXML = true;

                    while (validator.Read())
                    {
                        // empty body
                    }
                }
            }
        }
    }
}